
import unittest

class TestList(unittest.TestCase):
    
    def test_contain_multitype(self):
        studInfo = [1,"cheka",100.0]
        
        studId = studInfo[0]
        studName = studInfo[1]
        studScore = studInfo[2]
        
        self.assertEqual(1,studId)
        self.assertEqual("cheka",studName)
        self.assertAlmostEqual(100.0,studScore)
        
        # ----------- invalid index will throw exception
        self.assertRaises(IndexError,lambda : studInfo[10])
        
    def test_enumerate(self):
        name = "cheka"
        expected = ["c","h","e","k","a"]
        
        for index,ch in enumerate(name):
            self.assertEqual(expected[index],ch)
            
    def test_equality(self):
        """
        this testcase shows that for list object
        '==' checks equality based on content checking, rather that reference checking
        """
        list1 = [1,2]
        list2 = [1,2]
        
        self.assertNotEqual(id(list1),id(list2))
        self.assertFalse(list1 is list2)
        
        self.assertEqual(list1,list2)
        self.assertTrue(list1 == list2)
        
        # ================================= #
        atuple = (1,2)
        self.assertFalse(list1 == atuple) # even though with equal content, but tuple will never equal with list
        
        converted_list = list(atuple)
        self.assertEqual(list1,converted_list)
        self.assertFalse(list1 is converted_list)
        
    def test_reference_equal(self):
        """
        mutable type will never cache, always allocate a new memory space
        """
        list1 = []
        list2 = []
        self.assertTrue(list1 is not list2)
        
    def test_list_comprehension(self):
        squared = [x**2 for x in range(4) if x%2 == 0]
        self.assertEqual([0,4],squared)
        
        self.assertEqual([2,4,6],[item * 2 for item in range(1,4)])
        self.assertEqual([2,4,6,8],[item for item in range(1,10) if item % 2 == 0])
        
    def test_chain_by_add(self):
        record1 = [1,"cheka",100.0]
        record2 = [2,"stasi",99.9]
        
        self.assertEqual([1,"cheka",100.0,2,"stasi",99.9],record1 + record2)
        
    def test_mutable_feature(self):
        """
        this test shows that even items can be changed, but list itself is unchanged
        so call it 'mutable'
        """
        alist = [1,"cheka"]
        oriid = id(alist)
        
        alist[0] = 2
        alist.append(100.0)
        newid = id(alist)
        
        self.assertEqual(oriid,newid)
        
    def test_create_by_iterable(self):
        """
        this test shows that when list() is given a iterable object
        list will create a list and put each item in that iterable object into that created list
        """
        self.assertEqual(["c","h","e","k","a"],list("cheka"))
        
    def test_slice(self):
        alist = [1,"cheka",66]
        self.assertEqual(66,alist[-1])
        self.assertEqual(["cheka"],alist[1:-1])
        self.assertEqual([1,"cheka"],alist[:-1])
        
        # -------------- check that slice returns a totally new instance
        # -------------- not just a reference to the original list
        part_list = alist[:-1]
        part_list[0] = "new element"
        # since slice returns new, isolated instance, so change on the slice will not affect original list
        self.assertEqual(["new element","cheka"],part_list)
        self.assertEqual([1,"cheka",66],alist)
        
    def test_replace(self):
        """
        when replacing, the left handside is a single [index] or [startindex:endindex]
        is totally different
        alist[idnex] = [x,y] ===> alist[... [x,y], ...]
        alist[startindex:endindex] = [x,y] ===> alist[... x,y, ...]
        """
        alist = [1,"cheka",100.0,"stasi",99]
        
        # when subsitute a single item with empty list or empty tuple
        # it just replace that single element
        alist[1] = [] 
        self.assertEqual([1,[],100.0,"stasi",99],alist)
        
        # when subsitute a range with empty list or empty tuple
        # it equivalent to delete
        alist[1:2] = ()
        self.assertEqual([1,100.0,"stasi",99],alist)
        
        alist[1:3] = []
        self.assertEqual([1,99],alist)
        
        # when replace a range, the right hand-side can be any iterable
        alist[0:1] = (5,"new",2)
        self.assertEqual([5,"new",2,99],alist)
        
        alist[1:3] = "python"
        self.assertEqual([5,"p","y","t","h","o","n",99],alist)
        
        alist[1] = "cheka"
        self.assertEqual([5,"cheka","y","t","h","o","n",99],alist)
        
    def test_append(self):
        alist = [1,"cheka"]
        alist.append([2,100.0])
        self.assertEqual([1,"cheka",[2,100.0]],alist)
        
        # ---------- "alist.append(x)" is equivalent to "alist[len(alist):]=[x](or (x))"
        # ---------- we must put x inside [], then it will just add element 'x', rather than [x]
        alist[len(alist):] = ["new"]
        self.assertEqual([1,"cheka",[2,100.0],"new"],alist)
        
    def test_extend(self):
        alist = [1,"cheka"]
        alist.extend("ok")
        self.assertEqual([1,"cheka","o","k"],alist)
        
        # ---------- "extend" is equivalent to "alist[len(alist):] = x",where x is iterable
        alist[len(alist):] = [45,"new"]
        self.assertEqual([1,"cheka","o","k",45,"new"],alist)
        
        # ---------- "+" can also extend, but will create a new object to hold the concatenate result
        alist = [1]
        oriid = id(alist)
        
        # throw exception due to argument is not iterable
        self.assertRaises( TypeError, lambda : alist.extend(2))
        
        alist.extend([2])
        newid = id(alist)
        self.assertEqual(oriid,newid)
        
        alist = alist + [3]
        self.assertEqual([1,2,3],alist)
        newid = id(alist)
        # using "extend" will just change the same list, different from "+", which will create a new instance
        # and copy data from both parameter into that new instance, so "extend" is more efficient
        self.assertNotEqual(oriid,newid)
        
        # ---------- and "extend" can take a argument which is not the same type, as long as that object is iterable
        # ---------- but "+" cannot
        alist.extend("ok")
        self.assertEqual([1,2,3,"o","k"],alist)
        
        self.assertRaises(TypeError,lambda: alist + "ok") # only same type can "+"
        
    def test_mutiple(self):
        # =============== declare object for test
        class SimpleObject(object):
            def __init__(self):
                self.id = 0
                
        # =============== test
        objlist = [SimpleObject()] * 2
        
        # just shallow copy, not deep copy
        self.assertTrue(objlist[0] is objlist[1])
        
    def test_remove(self):
        alist = ["cheka","stasi","cheka"]
        alist.remove("cheka") # only remove the first matched item
        self.assertEqual(["stasi","cheka"],alist)
        
    def test_clear(self):
        alist = range(4)
        original_id = id(alist)
        
        del alist[:2]
        self.assertEqual([2,3],alist)
        
        del alist[:]
        self.assertEqual(0,len(alist))
        
        current_id = id(alist) 
        self.assertEqual(original_id,current_id)
        
    def test_insert(self):
        alist = [1,2,3]
        alist.insert(1,"cheka")
        self.assertEqual([1,"cheka",2,3],alist)
        
    def test_count(self):
        alist = [1,"cheka",1,"cheka",1,3,1]
        self.assertEqual(4,alist.count(1))
        self.assertEqual(2,alist.count("cheka"))
        self.assertEqual(1,alist.count(3))
        self.assertEqual(0,alist.count("NonExisted"))
        
    def test_index(self):
        alist = [1,"cheka",1,"cheka",1,3,1]
        self.assertEqual(0,alist.index(1))
        self.assertEqual(2,alist.index(1,1)) # always find the first position "k" where alist[k]==wanted,where start<=k<stop
        
        self.assertEqual(1,alist.index("cheka"),msg="find from position 0 to len(alist)")
        self.assertEqual(3,alist.index("cheka",2),msg="find from position 2 to len(alist)")
        
        self.assertRaises( ValueError, lambda : alist.index("NonExisted") )
        
    def test_sort(self):
        numlist = [6,4.78,1.2,5]
        numlist.sort()
        self.assertEqual([1.2,4.78,5,6],numlist)
        
        # ------------ reverse sort
        numlist.sort(reverse = True)
        self.assertEqual([6,5,4.78,1.2],numlist)
        
        strlist = ["kgb","mss","cheka"]
        strlist.sort()
        self.assertEqual(["cheka","kgb","mss"],strlist)
        
    def test_pop(self):
        # when no argument is given, default argument for "pop" is -1, standing for the last item
        alist = [1,2,"cheka",3,"stasi"]
        self.assertEqual("stasi",alist.pop())
        self.assertEqual([1,2,"cheka",3],alist)
        
        self.assertEqual(2,alist.pop(1))
        self.assertEqual([1,"cheka",3],alist)
    
    def test_del(self):
        alist = [1,"cheka",100.0,"stasi",99]
        
        # --------- delete single
        del alist[1]
        self.assertEqual([1,100.0,"stasi",99],alist)
        
        # --------- delete a range
        del alist[1:3]
        self.assertEqual([1,99],alist)
        
        # --------- delete equivalent to replace with []
        alist[0:1] = []
        self.assertEqual([99],alist)

    def test_sort_bykey(self):
        """
        difference between "list.sort" and built-in "sorted" is that
        "list.sort" sort the list in place
        while built-in "sorted" return a new list sorted by specific order
        """
        names = ["cheka","STASI","mss"]
        names.sort()
        self.assertEqual(["STASI","cheka","mss"],names)
        
        names.sort(key = str.lower)
        self.assertEqual(["cheka","mss","STASI"],names)
        
    def test_list(self):
        srclist = range(5)
        cpylist = srclist[:]
        
        self.assertEqual(srclist,cpylist)
        self.assertTrue(srclist is not cpylist)
        

class TestListComprehension(unittest.TestCase):
    """
    using list comprehension can do the same work as map, filter
    but in a much simplified manner
    """
    
    def test_as_filter(self):
        orilist = xrange(6)
        
        odd_list = [x for x in orilist if x%2==1]
        self.assertEqual([1,3,5],odd_list)
        
        even_list = [x for x in orilist if x%2==0]
        self.assertEqual([0,2,4],even_list)
        
    def test_as_map(self):
        orilist = xrange(4)
        
        self.assertEqual([0,1,4,9],[x*x for x in orilist])
        self.assertEqual([1,2,3,4],[x+1 for x in orilist])
        
    def test_multi_iter_variables(self):
        combine_result = [(x,y) for x in xrange(3) for y in xrange(2)]
        self.assertEqual([(0,0),(0,1),
                          (1,0),(1,1),
                          (2,0),(2,1)],combine_result)
        
    def test_select_many(self):
        """
        using list comprehension to simulate "SelectMany" feature in LINQ
        """
        full_names = ["Anne Williams","John Fred Smith"]
        expected_part_names = ["Anne","Williams","John","Fred","Smith"]
        actual_part_names = [partname for fullname in full_names for partname in fullname.split()]
        self.assertEqual(expected_part_names,actual_part_names)
        

if __name__ == "__main__":
    unittest.main()